/* Copyright (C) 2014 InfiniDB, Inc.

   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License
   as published by the Free Software Foundation; version 2 of
   the License.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
   MA 02110-1301, USA. */

/****************************************************************************
* $Id: func_date_add.cpp 3905 2013-06-18 12:27:32Z bwilkinson $
*
*
****************************************************************************/

#include <cstdlib>
#include <string>
using namespace std;

#include "boost/assign.hpp"
using namespace boost::assign;

#include "functor_dtm.h"
#include "functioncolumn.h"
#include "intervalcolumn.h"
#include "constantcolumn.h"
#include "rowgroup.h"
using namespace execplan;

#include "dataconvert.h"
using namespace dataconvert;

#include "funchelpers.h"

namespace funcexp
{
namespace helpers
{

uint64_t dateAdd( uint64_t time, const string& expr, IntervalColumn::interval_type unit, bool dateType, OpType funcType )
{
	int array[10];
	int64_t array2[10];

	int month_length[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
	
	int32_t year = 0, 
	         month = 0, 
	         day = 0, 
	         hour = 0, 
			 monthSave = 0;

	int64_t min = 0, 
	         sec = 0, 
	         msec = 0;

	int32_t yearAdd = 0, 
	         monthAdd = 0, 
	         dayAdd = 0, 
	         hourAdd = 0;

	int64_t minAdd = 0, 
	         secAdd = 0, 
	         msecAdd = 0;


	if (dateType)
	{
		year = (uint32_t)((time >> 16) & 0xffff);
		month = (uint32_t)((time >> 12) & 0xf);
		day = (uint32_t)((time >> 6) & 0x3f);
	}
	else
	{
		year = (uint32_t)((time >> 48) & 0xffff);
		month = (uint32_t)((time >> 44) & 0xf);
		day = (uint32_t)((time >> 38) & 0x3f);
		hour = (uint32_t)((time >> 32) & 0x3f);
		min = (uint32_t)((time >> 26) & 0x3f);
		sec = (uint32_t)((time >> 20) & 0x3f);
		msec = (uint32_t)((time & 0xfffff));
	}

	monthSave = month;

	int index = -1;
	
	switch( unit )
	{
		case IntervalColumn::INTERVAL_MINUTE:
		case IntervalColumn::INTERVAL_SECOND:
		case IntervalColumn::INTERVAL_MICROSECOND:
			index = getNumbers( expr, array2, funcType);
			break;
		default:
			index = getNumbers( expr, array, funcType);
			break;
	};
	
	
	if ( index <= 0 )
		throw runtime_error("expression type is not supported");

	switch( unit ) {
		case IntervalColumn::INTERVAL_YEAR:
			{
				yearAdd = array[0];
				break;
			}
		case IntervalColumn::INTERVAL_QUARTER:
			{
				monthAdd = array[0] * 3;
				break;
			}
		case IntervalColumn::INTERVAL_MONTH:
			{
				monthAdd = array[0];
				break;
			}
		case IntervalColumn::INTERVAL_WEEK:
			{
				dayAdd = array[0] *7;
				break;
			}
		case IntervalColumn::INTERVAL_DAY:
			{
				dayAdd = array[0];
				break;
			}
		case IntervalColumn::INTERVAL_HOUR:
			{
				hourAdd = array[0];
				break;
			}
		case IntervalColumn::INTERVAL_MINUTE:
			{
				minAdd = array2[0];
				break;
			}
		case IntervalColumn::INTERVAL_SECOND:
			{
				secAdd = array2[0];
				break;
			}
		case IntervalColumn::INTERVAL_MICROSECOND:
			{
				msecAdd = array2[0];
				break;
			}
		case IntervalColumn::INTERVAL_YEAR_MONTH:
			{
				if ( index > 2 ) {
					return 0;
				}
				if ( index == 1 )
					monthAdd = array[0];
				else
				{
					yearAdd = array[0];
					monthAdd = array[1];
				}

				break;
			}
		case IntervalColumn::INTERVAL_DAY_HOUR:
			{
				if ( index > 2 ) {
					return 0;
				}
				if ( index == 1 )
					hourAdd = array[0];
				else
				{
					dayAdd = array[0];
					hourAdd = array[1];
				}

				break;
			}
		case IntervalColumn::INTERVAL_DAY_MINUTE:
			{
				if ( index > 3 ) {
					return 0;
				}
				if ( index == 1 )
					minAdd = array[0];
				else
				{
					if ( index == 2 )
					{
						hourAdd = array[0];
						minAdd = array[1];
					}
					else
					{
						dayAdd = array[0];
						hourAdd = array[1];
						minAdd = array[2];
					}
				}

				break;
			}
		case IntervalColumn::INTERVAL_DAY_SECOND:
			{
				if ( index > 4 ) {
					return 0;
				}
				if ( index == 1 )
					secAdd = array[0];
				else
				{
					if ( index == 2 )
					{
						minAdd = array[0];
						secAdd = array[1];
					}
					else
					{
						if ( index == 3 )
						{
							hourAdd = array[0];
							minAdd = array[1];
							secAdd = array[2];
						}
						else
						{
							dayAdd = array[0];
							hourAdd = array[1];
							minAdd = array[2];
							secAdd = array[3];
						}
					}
				}

				break;
			}
		case IntervalColumn::INTERVAL_HOUR_MINUTE:
			{
				if ( index > 2 ) {
					return 0;
				}
				if ( index == 1 )
					minAdd = array[0];
				else
				{
					hourAdd = array[0];
					minAdd = array[1];
				}

				break;
			}
		case IntervalColumn::INTERVAL_HOUR_SECOND:
			{
				if ( index > 3 ) {
					return 0;
				}
				if ( index == 1 )
					secAdd = array[0];
				else
				{
					if ( index == 2 )
					{
						minAdd = array[0];
						secAdd = array[1];
					}
					else
					{
						hourAdd = array[0];
						minAdd = array[1];
						secAdd = array[2];
					}
				}

				break;
			}
		case IntervalColumn::INTERVAL_MINUTE_SECOND:
			{
				if ( index > 2 ) {
					return 0;
				}
				if ( index == 1 )
					secAdd = array[0];
				else
				{
					minAdd = array[0];
					secAdd = array[1];
				}

				break;
			}
		case IntervalColumn::INTERVAL_DAY_MICROSECOND:
			{
				if ( index > 5 ) {
					return 0;
				}
				if ( index == 1 )
					msecAdd = array[0];
				else
				{
					if ( index == 2 )
					{
						secAdd = array[0];
						msecAdd = array[1];
					}
					else
					{
						if ( index == 3 )
						{
							minAdd = array[0];
							secAdd = array[1];
							msecAdd = array[2];
						}
						else
						{
							if ( index == 4 )
							{
								hourAdd = array[0];
								minAdd = array[1];
								secAdd = array[2];
								msecAdd = array[3];
							}
							else
							{
								dayAdd = array[0];
								hourAdd = array[1];
								minAdd = array[2];
								secAdd = array[3];
								msecAdd = array[4];
							}
						}
					}
				}

				break;
			}
		case IntervalColumn::INTERVAL_HOUR_MICROSECOND:
			{
				if ( index > 4 ) {
					return 0;
				}
				if ( index == 1 )
					msecAdd = array[0];
				else
				{
					if ( index == 2 )
					{
						secAdd = array[0];
						msecAdd = array[1];
					}
					else
					{
						if ( index == 3 )
						{
							minAdd = array[0];
							secAdd = array[1];
							msecAdd = array[2];
						}
						else
						{
							hourAdd = array[0];
							minAdd = array[1];
							secAdd = array[2];
							msecAdd = array[3];
						}
					}
				}

				break;
			}
		case IntervalColumn::INTERVAL_MINUTE_MICROSECOND:
			{
				if ( index > 3 ) {
					return 0;
				}
				if ( index == 1 )
					msecAdd = array[0];
				else
				{
					if ( index == 2 )
					{
						secAdd = array[0];
						msecAdd = array[1];
					}
					else
					{
						minAdd = array[0];
						secAdd = array[1];
						msecAdd = array[2];
					}
				}

				break;
			}
		case IntervalColumn::INTERVAL_SECOND_MICROSECOND:
			{
				if ( index > 2 ) {
					return 0;
				}
				if ( index == 1 )
					msecAdd = array[0];
				else
				{
					secAdd = array[0];
					msecAdd = array[1];
				}

				break;
			}
		default:
			// should be impossible to get here since we checked the presence
			// in the map, but just to be safe...
			throw runtime_error("unit type is not supported");
	};


	// calulate new date

	year += yearAdd;
	month += monthAdd;
	day += dayAdd;
	hour += hourAdd;
	min += minAdd;
	sec += secAdd;
	msec += msecAdd;

	if ( msec > 999999 ) {
		int64_t secs = msec / 1000000;
		sec += secs;
		msec = msec - ( secs * 1000000 );
	}

	if ( msec < 0 ) {
		int64_t secs = 1 + (-msec / 1000000);
		sec -= secs;
		msec = msec + ( secs * 1000000 );
	}

	if ( sec > 59 ) {
		int64_t mins = sec / 60;
		min += mins;
		sec = sec - ( mins * 60 );
	}

	if ( sec < 0 ) {
		int64_t mins = 0;
		if ( ( sec + (-sec / 60) * 60 ) == 0 &&
			sec != 0)
			mins = (-sec / 60);
		else
			mins = 1 + (-sec / 60);

		min -= mins;
		sec = sec + ( mins * 60 );
		if ( sec >= 60 )
			sec = 0;
	}

	if ( min > 59 ) {
		int hours = min / 60;
		hour += hours;
		min = min - ( hours * 60 );
	}

	if ( min < 0 ) {
		int hours = 0;
		if ( ( min + (-min / 60) * 60 ) == 0 &&
			min != 0)
			hours = (-min / 60);
		else
			hours = 1 + (-min / 60);

		hour -= hours;
		min = min + ( hours * 60 );
		if ( min >= 60 )
			min = 0;
	}

	if ( hour > 23 ) {
		int days = hour / 24;
		day += days;
		hour = hour - ( days * 24 );
	}

	if ( hour < 0 ) {
		int days = 0;
		if ( ( hour + (-hour / 24) * 24 ) == 0 &&
			hour != 0)
			days = (-hour / 24);
		else
			days = 1 + (-hour / 24);

		day -= days;
		hour = hour + ( days * 24 );
		if ( hour >= 24 )
			hour = 0;
	}

	int tmpYear = year;
	if ( isLeapYear(tmpYear) )
		month_length[2] = 29;
	else
		month_length[2] = 28;

	if ( day > 0 ) {
		while(true)
		{
			int years = (monthSave-1) / 12;
//			tmpYear += years;
			if ( isLeapYear(tmpYear) )
				month_length[2] = 29;
			else
				month_length[2] = 28;

			int tmpMonth = monthSave - ( years * 12 );
			if ( day <= month_length[tmpMonth] )
				break;
			month++;
			day = day - month_length[tmpMonth];
			monthSave++;
			if ( monthSave > 12 ) {
				monthSave = 1;
				tmpYear++;
			}
		}
	}
	else
	{
		while(true)
		{
			if ( day > 0 )
				break;
			if ( -day < month_length[monthSave] ) {
				month--;
				monthSave--;
				if ( monthSave == 0 ) {
					monthSave = 12;
					tmpYear--;
				}
				if (monthSave == 2) {
			//		if ( dataconvert::DataConvert::isLeapYear(year) )
					if ( isLeapYear(tmpYear) )
						month_length[2] = 29;
				}
				if (day < 1 )
					day = month_length[monthSave] + day;
				// BUG 5448 - changed from '==' to '<='
				if ( day <= 0 ) {
					month--;
					monthSave--;
					if ( monthSave == 0 ) {
						monthSave = 12;
						tmpYear--;
					}
					if (monthSave == 2) {
				//		if ( dataconvert::DataConvert::isLeapYear(year) )
						if ( isLeapYear(tmpYear) )
							month_length[2] = 29;
					}
					day = day + month_length[monthSave];
				}
				break;
			}

			month--;
			monthSave--;
			if ( monthSave == 0 ) {
				monthSave = 12;
				tmpYear--;
				if ( isLeapYear(tmpYear) )
					month_length[2] = 29;
				else
					month_length[2] = 28;
			}
			day = day + month_length[monthSave];
		}
	}


	if ( month > 12 ) {
		int years = (month-1) / 12;
		year += years;
		month = month - ( years * 12 );
	}

	if ( month < 1 ) {
		int years = 1 + ((-month) / 12);
		year -= years;
		month = month + ( years * 12 );
	}

	if ( isLeapYear(year) )
		month_length[2] = 29;
	else
		month_length[2] = 28;

	if ( day > month_length[month] )
		day = month_length[month];

	if ( year < 1000 || year > 9999 ) {
		return 0;
	}


	uint64_t value;
	dataconvert::DateTime aDatetime;
	aDatetime.year = year;
	aDatetime.month = month;
	aDatetime.day = day;
	aDatetime.hour = hour;
	aDatetime.minute = min;
	aDatetime.second = sec;
	aDatetime.msecond = msec;
	value = *(reinterpret_cast<uint64_t*>(&aDatetime));

	return value;

}
} // namespace funcexp::helpers

CalpontSystemCatalog::ColType Func_date_add::operationType( FunctionParm& fp, CalpontSystemCatalog::ColType& resultType )
{
	resultType.colDataType = CalpontSystemCatalog::DATETIME;
	resultType.colWidth = 8;

	return resultType;
}


int64_t Func_date_add::getIntVal(rowgroup::Row& row,
							FunctionParm& parm,
							bool& isNull,
							CalpontSystemCatalog::ColType& ct)
{
	int64_t val = 0;
	bool dateType = false;
	switch (parm[0]->data()->resultType().colDataType)
	{
		case execplan::CalpontSystemCatalog::BIGINT:
		case execplan::CalpontSystemCatalog::INT:
		case execplan::CalpontSystemCatalog::MEDINT:
		case execplan::CalpontSystemCatalog::TINYINT:
		case execplan::CalpontSystemCatalog::SMALLINT:
		{
			val = dataconvert::DataConvert::intToDatetime(parm[0]->data()->getIntVal(row, isNull));
			break;
		}
		case execplan::CalpontSystemCatalog::DECIMAL:
		{
			if (parm[0]->data()->resultType().scale)
				val = dataconvert::DataConvert::intToDatetime(parm[0]->data()->getIntVal(row, isNull));
			break;
		}
		case execplan::CalpontSystemCatalog::VARCHAR:
		case execplan::CalpontSystemCatalog::CHAR:
		{
			val = dataconvert::DataConvert::stringToDatetime(parm[0]->data()->getStrVal(row, isNull));
			break;
		}
		case execplan::CalpontSystemCatalog::DATE:
		{
			val = parm[0]->data()->getDateIntVal(row, isNull);
			dateType = true;
			break;
		}
		case execplan::CalpontSystemCatalog::DATETIME:
		{
			val = parm[0]->data()->getDatetimeIntVal(row, isNull);
			break;
		}
		default:
		{
			isNull = true;
		}
	}
	
	if (isNull || val == -1)
	{
		isNull = true;
		return 0;
	}	
	
	IntervalColumn::interval_type unit = static_cast<IntervalColumn::interval_type>(parm[2]->data()->getIntVal(row, isNull));
	OpType funcType = OP_ADD;
	ConstantColumn* constCol = dynamic_cast<ConstantColumn*>(parm[3]->data());
	execplan::CalpontSystemCatalog::ColType ct3 = parm[3]->data()->resultType();
	if ((ct3.colDataType == execplan::CalpontSystemCatalog::CHAR ||
	     ct3.colDataType == execplan::CalpontSystemCatalog::VARCHAR) &&
	    constCol != NULL && constCol->constval().compare("SUB") == 0)
		funcType = OP_SUB;
	else
		funcType = static_cast<OpType>(parm[3]->data()->getIntVal(row, isNull));

	uint64_t value = helpers::dateAdd(
						val, parm[1]->data()->getStrVal(row, isNull), unit, dateType, funcType);

	if ( value == 0 )
		isNull = true;

//	if (dateType)
//		value = (((value >> 32) & 0xFFFFFFC0) | 0x3E);

	return value;
}


string Func_date_add::getStrVal(rowgroup::Row& row,
							FunctionParm& parm,
							bool& isNull,
							CalpontSystemCatalog::ColType& ct)
{
	return dataconvert::DataConvert::datetimeToString(getIntVal(row, parm, isNull, ct));

}


} // namespace funcexp
// vim:ts=4 sw=4:
